package com.huawei.ds.patch;

import com.alibaba.fastjson.JSONObject;
import net.lingala.zip4j.ZipFile;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.log4j.Logger;

import java.io.*;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.util.Arrays;

import static com.huawei.ds.patch.PatchMain.*;

public class PatchAdapter {
    private static final long REQUIRED_FREE_SPACE = 2_000_000_000l;
    private static Logger logger = PatchMain.logger();

    private static final String OS_WIN = "win";
    private static final String OS_UNIX = "unix";
    private static final String OS_MAC = "mac";
    private static final String OS_UNKNOWN = "unknown";

    private static final String ARCHIVE_RELATIVE_PATH_DEFAULT = "MindStudio";
    private static final String ARCHIVE_RELATIVE_PATH1_ORIGIN_MAC = "DecEco Studio.app";
    private static final String ARCHIVE_RELATIVE_PATH1_MAC = "DecEco-Studio.app";
    private static final String ARCHIVE_RELATIVE_PATH2_MAC = "Contents";

    public static boolean build(PatchOptions po) {
        try {
            String action = po.getAction();
            String updaterJar = po.getUpdaterJar();
            String updaterJarDir = po.getUpdaterJarDir();
            File tempDir = po.getTempWorkspaceDir();
            switch(action) {
                case ACTION_CREATE:
                    String oldResource = po.getOldVersionPath();
                    String newResource = po.getNewVersionPath();
                    PatchMain.startUIProcess("Starting to process old version file...");
                    String oldNormalizedResource = processResource(oldResource, tempDir);
                    PatchMain.startUIProcess("Starting to process new version file...");
                    String newNormalizedResource = processResource(newResource, tempDir);

                    if (oldNormalizedResource == null || newNormalizedResource == null) {
                        logger.error("Resource is null:maybe download error or zip not exists");
                        return false;
                    }

                    String osName = resolveOsName(oldResource, newResource);
                    if (osName.equals(OS_UNKNOWN)) {
                        logger.error("Resource are not valid");
                        return false;
                    }

                    String oldBuildNumber = resolveBuildNumber(oldNormalizedResource, osName);
                    String newBuildNumber = resolveBuildNumber(newNormalizedResource, osName);
                    String outputPatch = updaterJarDir + buildOutputPatchName(oldBuildNumber, newBuildNumber, osName);
                    String[] originArgs = po.getOriginArgs();
                    originArgs[1] = StringUtils.isBlank(originArgs[1])?oldBuildNumber:originArgs[1];
                    originArgs[2] = StringUtils.isBlank(originArgs[2])?newBuildNumber:originArgs[2];
                    String[] newArgs = buildCreateArgs(po.getOriginArgs(), oldNormalizedResource, newNormalizedResource, outputPatch, new String[]{"--jar--" + updaterJar});

                    PatchMain.startUIProcess("originArgs is..." + Arrays.toString(originArgs));
                    invokeUpdaterRunner(newArgs, updaterJar, tempDir);
                    break;
                case ACTION_NULL:
                    invokeUpdaterRunner(null, updaterJar, tempDir);
                    break;
                case ACTION_OTHER:
                    invokeUpdaterRunner(po.getOriginArgs(), updaterJar, tempDir);
                    break;
                default:
            }
            return true;
        } catch (Exception e) {
            logger.error("MS updater failed: " + e.getMessage());
            return false;
        }
    }

    public static String resolveOsName(String oldResource, String newResource) {
        String os1 = resolveOsName(oldResource).getKey();
        String os2 = resolveOsName(newResource).getKey();
        return StringUtils.isNotBlank(os1) && StringUtils.equals(os1, os2)?os1:OS_UNKNOWN;
    }

    public static Pair<String, Integer> resolveOsName(String resource) {
        String resourcePath = resource.toLowerCase();
        if (resourcePath.endsWith(".win.zip")) {
            return Pair.of(OS_WIN, 8);
        }
        else if (resourcePath.endsWith(".tar.gz")) {
            return Pair.of(OS_UNIX, 7);
        }
        else if (resourcePath.endsWith(".mac.zip")) {
            return Pair.of(OS_MAC, 8);
        }
        else {
            return Pair.of("", 0);
        }
    }

    public static String resolveBuildNumber(String resourcePath, String osName) {
        try {
            if (osName.equals(OS_MAC)) {
                resourcePath += (File.separator + "Resources");
            }
            if (osName.equals(OS_WIN)) {
                resourcePath = resourcePath.substring(0, resourcePath.length()-2);
            }

            File buildFile = new File(resourcePath, "build.txt");
            String buildNumber = FileUtils.readFileToString(buildFile, "UTF-8");
            if (StringUtils.isNotBlank(buildNumber)) {
                String[] parts = buildNumber.split("-");
                return parts[1];
            }

            File producuInfoFile = new File(resourcePath, "product-info.json");
            String productInfoJson = FileUtils.readFileToString(producuInfoFile, "UTF-8");
            JSONObject jsonObj = JSONObject.parseObject(productInfoJson);
            buildNumber = jsonObj.getString("buildNumber");
            return buildNumber;
        } catch (Exception e) {
            logger.error("Failed to resolve build number from build.txt or product-info.json");
            return null;
        }
    }

    private static String buildOutputPatchName(String oldVersionDesc, String newVersionDesc, String osName) {
        return String.format("MS-%S-%s-patch-%s.jar", oldVersionDesc, newVersionDesc, osName);
    }

    private static File getTempFile(File tempDir, String name) throws IOException {
        File myTempFile;
        int index = 0;
        do {
            myTempFile = new File(tempDir, name + ".tmp." + index++);
        } while (myTempFile.exists());
        return myTempFile;
    }

    private static String[] buildCreateArgs(String[] args, String replace3rdValue, String replace4thValue, String insert5thValue, String[] appends) {
        int originLen = args.length;
        int appendLen = appends != null ? appends.length : 0;
        String[] newArgs = Arrays.copyOf(args, originLen + appendLen + 1);
        newArgs[3] = replace3rdValue;
        newArgs[4] = replace4thValue;

        for (int i = originLen; i > 5; --i) {
            newArgs[i] = newArgs[i - 1];
        }
        newArgs[5] = insert5thValue;

        for (int i = originLen + 1; i < newArgs.length; i++) {
            newArgs[i] = appends[i - originLen - 1];
        }
        return newArgs;
    }

    private static String processResource(String resourcePath, File tempDir) throws IOException {
        if (resourcePath == null) {
            return null;
        }

        String lowerResourcePath = resourcePath.toLowerCase();
        if (lowerResourcePath.startsWith("http://") || lowerResourcePath.startsWith("https://")) {
            Pair<String, Integer> osName = resolveOsName(lowerResourcePath);
            if (StringUtils.isBlank(osName.getKey())) {
                return null;
            }
            String localResourcePath = downloadResource(resourcePath, tempDir);
            return unzipToTempDir(localResourcePath, tempDir, osName.getKey());
        }
        else if (lowerResourcePath.endsWith(".win.zip")) {
            return unzipToTempDir(resourcePath, tempDir, OS_WIN);
        }
        else if (lowerResourcePath.endsWith(".tar.gz")) {
            return untarToTempDir(resourcePath, tempDir);
        }
        else {
            return resourcePath;
        }
    }

    private static String downloadResource(String resourceUrl, File tempDir) throws IOException {
        String name = extractName(resourceUrl);
        File localTempZipFile = getTempFile(tempDir, name);
        URL url = new URL(resourceUrl);
        try (ReadableByteChannel readableByteChannel = Channels.newChannel(url.openStream());
            FileOutputStream fileOutputStream = new FileOutputStream(localTempZipFile);
            FileChannel fileChannel = fileOutputStream.getChannel()) {
            fileChannel.transferFrom(readableByteChannel, 0, Long.MAX_VALUE);
        }
        return localTempZipFile.getCanonicalPath();
    }

    private static String untarToTempDir(String resourceTarPath, File tempDir) throws IOException {
        String name = extractName(resourceTarPath);
        String nameWithoutSuffix = name.substring(0, name.indexOf(".tar.gz"));
        nameWithoutSuffix += String.format("-%s", OS_UNIX);
        File exist = existStartsWith(nameWithoutSuffix, tempDir);
        if (exist != null) {
            return normalizeDecompressDir(exist.getCanonicalPath(), OS_UNIX, false);
        }
        File tempOutputDir = getTempFile(tempDir, nameWithoutSuffix);
        try (TarArchiveInputStream fin = new TarArchiveInputStream(new GzipCompressorInputStream(new FileInputStream(resourceTarPath)))) {
            TarArchiveEntry entry;
            while ((entry = fin.getNextTarEntry()) != null) {
                if (entry.isDirectory()) {
                    continue;
                }
                File curfile = new File(tempOutputDir, entry.getName());
                File parent = curfile.getParentFile();
                if (!parent.exists()) {
                    parent.mkdirs();
                }
                OutputStream outputStream = new FileOutputStream(curfile);
                IOUtils.copy(fin, outputStream);
                outputStream.close();
            }
        }
        return normalizeDecompressDir(tempOutputDir.getCanonicalPath(), OS_UNIX, false);
    }

    private static String unzipToTempDir(String resourceZipPath, File tempDir, String osName) throws IOException {
        String name = extractName(resourceZipPath);
        String nameWithoutSuffix = name.substring(0, name.indexOf("." + osName));
        nameWithoutSuffix += String.format("-%s", osName);
        File exist = existStartsWith(nameWithoutSuffix, tempDir);
        if (exist != null) {
            return normalizeDecompressDir(exist.getCanonicalPath(), osName, false);
        }
        File tempUnzipDir = getTempFile(tempDir, nameWithoutSuffix);
        String tempUnzipDirPath = tempUnzipDir.getCanonicalPath();
        ZipFile zipFile = new ZipFile(resourceZipPath);
        zipFile.extractAll(tempUnzipDirPath);
        return normalizeDecompressDir(tempUnzipDirPath, osName, true);
    }

    private static File existStartsWith(String prefix, File tempDir) throws IOException {
        File[] files = tempDir.listFiles();
        for (File file : files) {
            String name = file.getName();
            if (StringUtils.startsWith(name, prefix)) {
                return file;
            }
        }
        return null;
    }

    private static String normalizeDecompressDir(String decompressDir, String osName, boolean needHandleSpace) {
        if (osName.equals(OS_MAC) && needHandleSpace) {
            File oldStudioDir = new File(decompressDir, ARCHIVE_RELATIVE_PATH1_ORIGIN_MAC);
            File newStudioDir = new File(decompressDir, ARCHIVE_RELATIVE_PATH1_MAC);
            if (!oldStudioDir.exists()) {
                PatchMain.startUIProcess("Error:Mac decompressed studio path does not exist");
            }
            else {
                boolean success = oldStudioDir.renameTo(newStudioDir);
                PatchMain.startUIProcess(success ? "Mac studio path is...." : "Error:Mac studio path.....");
            }
        }
        String suffix = osName.equals(OS_WIN) || osName.equals(OS_UNIX) ? ARCHIVE_RELATIVE_PATH_DEFAULT
                : osName.equals(OS_MAC) ? ARCHIVE_RELATIVE_PATH1_MAC + File.separator + ARCHIVE_RELATIVE_PATH2_MAC : "";
        if (osName.equals(OS_WIN)) {
            suffix = " ";
        }
        return decompressDir + File.separator + suffix;
    }

    private static void invokeUpdaterRunner(String[] args, String updaterJar, File tempDir) throws IOException {
        String updaterTempPrefix = "updater-jar-dir";
        File tempPatchDir = existStartsWith(updaterTempPrefix, tempDir);
        if (tempPatchDir == null) {
            tempPatchDir = getTempFile(tempDir, updaterTempPrefix);
            FileUtils.copyFileToDirectory(new File(updaterJar), tempPatchDir);
        }
        File[] patchJarFile = tempPatchDir.listFiles();
        if (patchJarFile == null || patchJarFile.length != 1) {
            return;
        }

        CommandLine cmdLine = new CommandLine("java");
        cmdLine.addArgument("-cp");
        cmdLine.addArgument(patchJarFile[0].getCanonicalPath());
        cmdLine.addArgument("com.intellij.updater.Runner");

        for (String arg : args) {
            cmdLine.addArgument(arg);
        }
        DefaultExecutor executor = new DefaultExecutor();
        executor.setExitValue(0);
        PatchMain.startUIProcess("cmdLine is:" + cmdLine);
        int exitValue = executor.execute(cmdLine);
        if (exitValue == 0) {
            logger.info("Ending patch successfully");
        }
        else {
            logger.info("Ending path with failure");
        }
    }

    private static String extractName(String url) {
        String[] arr = url.split("[\\\\/]");
        return arr.length == 0 ? null : arr[arr.length - 1];
    }
}
